#include <iostream>
#include <fstream>
#include <string>
#include <GL\glut.h>

using namespace std;

// Functions Declarations

void Timer(int);
void moveForward();
void moveBackward();

// Global Variable Declarations

string fileName = "PainDeSucre.dem"; // The relative path to the file to use to build the elevation matrix from
int A[41][34];						// The matrix of elevations of the Pain de Sucre mountain created from the .dem file
int count = 0;						// total amount of vertices for the mountain which is also the amount of elevation points from the .dem file

float longitudeSpacing = 16.4;	// east-west spacing between elevation data in meters
float latitudeSpacing = 23.2;	// north-south spacing between elevation data in meters

int fovy = 45;		// Field of View Y of the Perspective
float ratio = 1;	// Ratio of the Perspective
float nPlane = 0.1;	// Near clipping plane
float fPlane = 2000; // Far clipping plane

int cameraSpeed = 5;	// Speed at which the camera moves forward and backward
int zoomSpeed = 2;		// Speed at which the camera zooms in and out

float posX = 0;			// Initial position of the camera in X
float posY = 550;		// Initial position of the camera in Y
float posZ = 0;			// Initial position of the camera in Z
float viewX = 580;		// X View position (highest peak of mountain)
float viewY = 407;		// Y View position (highest peak of mountain)
float viewZ = -213.2;	// Z View position (highest peak of mountain)

bool spin = false;		// when true, mountains spins
GLfloat spinAngle = 0;	// current angle at which the mountain is "spinned"

/*
* Draws a 41X34 grid of triangles.
* The spacing on the X axis is determined by the longitude spacing
* and on the Z axis by the latitude spacing. The Y coordinates are taken
* from the elevation data of the mountain.

* This results in an accurate representation of the mountain visually.
*/
void drawMountain()
{
	// Since we have no lighting model, the mountain looks flat, by drawing the mountain
	// in wireFrame it helps us properly see the elevation of the mountain.
	//glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );	// Required so that we can better see the elevation in the mountain.

	int c1 = 0; // red color
	int c2 = 0; // green color

	// The following loops are used to enclose the mountain surface
	glColor3f(0.8,0.5,0.5);
	glBegin(GL_TRIANGLE_STRIP);
	for(int row = 0; row != 41-1; ++row)
	{
		glVertex3f(0, A[row][0], row*latitudeSpacing);
		glVertex3f(0, 0, row*latitudeSpacing);
		glVertex3f(0, A[row+1][0], (row+1)*latitudeSpacing);

		glVertex3f(0, 0, (row+1)*latitudeSpacing);
	}
	glEnd();

	glBegin(GL_TRIANGLE_STRIP);
	for(int col = 0; col != 34-1; ++col)
	{
		glVertex3f(col*longitudeSpacing, A[0][col], 0);
		glVertex3f((col+1)*longitudeSpacing, A[0][col+1], 0);
		glVertex3f(col*longitudeSpacing, 0, 0);

		glVertex3f((col+1)*longitudeSpacing, 0, 0);
	}
	glEnd();
	
	glBegin(GL_TRIANGLE_STRIP);
	for(int row = 0; row != 41-1; ++row)
	{
		glVertex3f(33*longitudeSpacing, A[row][33], row*latitudeSpacing);
		glVertex3f(33*longitudeSpacing, 0, row*latitudeSpacing);
		glVertex3f(33*longitudeSpacing, A[row+1][33], (row+1)*latitudeSpacing);

		glVertex3f(33*longitudeSpacing, 0, (row+1)*latitudeSpacing);
	}
	glEnd();
	
	glBegin(GL_TRIANGLE_STRIP);
	for(int col = 0; col != 34-1; ++col)
	{
		glVertex3f(col*longitudeSpacing, A[40][col], 40*latitudeSpacing);
		glVertex3f((col+1)*longitudeSpacing, A[40][col+1], 40*latitudeSpacing);
		glVertex3f(col*longitudeSpacing, 0, 40*latitudeSpacing);

		glVertex3f((col+1)*longitudeSpacing, 0, 40*latitudeSpacing);
	}
	glEnd();
	
	// Iterating on rows and columns of the elevation matrix A we will
	// draw the triangles of the grid. We must not loop on every row and col, because
	// we are drawing 3 vertices on each iteration, thus we stop on row - 1 and col - 1.
	// One triangle strip is used per row, this saves on computational power, because it reduces
	// the amount of calls to glVertex3f();
	// NOTE: The mountain is drawn with triangles facing UP (0,1,0) using CCW mode.
	for(int row = 0; row != 41-1; ++row)
	{
		c1 = c1 + 2 % 255; // increment color each row

		glBegin(GL_TRIANGLE_STRIP);		// Begin triangle strip on each row
			for(int col = 0; col != 34-1+1; ++col)
			{
				c2 = c2 + 2 % 255; // increment color each col

				// Series of if condition that identifies the side of the mountain
				// and sets it to a specific color as required by the assignment.
				if(col == 34-1) glColor3f(0,1,0);
				else if(row == 0) glColor3f(1,1,0);
				else if(row == 41-2) glColor3f(0,1,1);
				else if(col == 0) glColor3f(0,0,1);
				else glColor3b(c1,c2,255);

				// Special case for first column of each row. This is the start of each triangle
				// strip, and therefore we must specify all vertix of the intial triangle.
				if(col == 0)
				{
					glVertex3f(col*longitudeSpacing, A[row][col], row*latitudeSpacing);
					glVertex3f(col*longitudeSpacing, A[row+1][col], (row+1)*latitudeSpacing);
					glVertex3f((col+1)*longitudeSpacing, A[row][col+1], row*latitudeSpacing);
				}
				// Special case for the second column of each row. Since we have already specified both
				// vertex of this triangle on col == 0, we need only add one extra vertex
				else if(col == 1)
				{
					glVertex3f(col*longitudeSpacing, A[row+1][col], (row+1)*latitudeSpacing);
				}
				// Normal case, for all remaining columns. One vertex was defined in the last iteration,
				// so two more vetrices must be defined to draw a triangle.
				else
				{
					glVertex3f(col*longitudeSpacing, A[row][col], row*latitudeSpacing);
					glVertex3f(col*longitudeSpacing, A[row+1][col], (row+1)*latitudeSpacing);
				}
			}
		glEnd();	// End triangle strip at the end of each row
	}

	//glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );	// Reset PolygonMode to default
}

void display()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);		// Clear buffers, we don't want stuff to draw over each other

	glPushMatrix();
	gluLookAt(posX,posY,posZ, viewX, viewY, viewZ, 0,1,0);

	glPushMatrix();
	glRotatef(90,0,1,0);	// Rotate mountain so that it shows in the positive X, negative Z, positive Y octant

	// Spin on Y axis at highest peak, which is found at A[25][13]
	glTranslatef((13*longitudeSpacing), 0, (25*latitudeSpacing));
	glRotatef(spinAngle, 0,1,0);
	glTranslatef(-(13*longitudeSpacing), 0, -(25*latitudeSpacing));

	drawMountain();
	glPopMatrix();

	glPopMatrix();

	glutSwapBuffers();
}

void reshape(int w, int h)
{
	if (h == 0) h = 1;		// Protect against division by zero, in case height = 0
	ratio = (float) w/h;	// Get aspect rato based on w and h of window

	// Set the PROJECTION matrix so that it reflects the new ratio
	glMatrixMode(GL_PROJECTION);

	glViewport(0, 0, w, h); // Viewport fills window
	glLoadIdentity();
	gluPerspective(fovy,ratio,nPlane,fPlane); // projection will have perspective

	glMatrixMode(GL_MODELVIEW); // Reset default MatrixMode
}

/*
* Apply changes to fovy to the PROJECTION matrix
*/
void resetZoom()
{
	glMatrixMode(GL_PROJECTION);

	glLoadIdentity();
	gluPerspective(fovy,ratio,nPlane,fPlane);

	glMatrixMode(GL_MODELVIEW);
}

void keyboard(unsigned char key, int x, int y)
{
	switch(key)
	{
		case '+':	// zoom in
			fovy -= cameraSpeed;
			resetZoom();
			break;
		case '-':	// zoom out
			fovy += cameraSpeed;
			resetZoom();
			break;
		case 'f':	// forward
			moveForward();
			break;
		case 'b':	// backward
			moveBackward();
			break;
		case 's':	// set spin on/off
			spin = !spin;
			glutTimerFunc(35,Timer,0);
			break;
	}

	glutPostRedisplay();
}

/*
* Strafe camera forward
*/
void moveForward() 
{
	// Get vector on line
    float vecX = viewX - posX;
    float vecY = viewY - posY;
    float vecZ = viewZ - posZ;

	float vecLength = sqrtf(powf(vecX,2) + powf(vecY,2) + powf(vecZ,2));

	// Normalise vector
	vecX = vecX / vecLength;
	vecY = vecY / vecLength;
	vecZ = vecZ / vecLength;

	// Move position of camera and view point
    posX += vecX * cameraSpeed;
    posY += vecY * cameraSpeed;
    posZ += vecZ * cameraSpeed;

    viewX += vecX * cameraSpeed;
    viewY += vecY * cameraSpeed;
    viewZ += vecZ * cameraSpeed;
}

/*
* Strafe camera backward
*/
void moveBackward() 
{
	// Get vector on line
    float vecX = viewX - posX;
    float vecY = viewY - posY;
    float vecZ = viewZ - posZ;

	float vecLength = sqrtf(powf(vecX,2) + powf(vecY,2) + powf(vecZ,2));

	// Normalise vector
	vecX = vecX / vecLength;
	vecY = vecY / vecLength;
	vecZ = vecZ / vecLength;

	// Move position of camera and view point
    posX -= vecX * cameraSpeed;
    posY -= vecY * cameraSpeed;
    posZ -= vecZ * cameraSpeed;

    viewX -= vecX * cameraSpeed;
    viewY -= vecY * cameraSpeed;
    viewZ -= vecZ * cameraSpeed;
}

/*
* Used to animate the spin of the mountain based on time
*/
void Timer(int value)
{
	if(spin)
	{
		spinAngle = spinAngle + cameraSpeed;
		glutPostRedisplay();

		glutTimerFunc(35,Timer,0);	// Must re-add event listener, because it automatically removes itself at each Timer event
	}
}

void main(int argc, char **argv)
{
	// Read in the elevation data from the file
	ifstream fin;
	fin.open(fileName.c_str());
	if (!fin)
	{
		cerr << "Failed to open " << fileName << endl;
		return;
	}

	int elevation = 0;
	int row = 0;
	int col = 0;
	while (fin >> elevation)
	{
		row = count / 34;
		col = count % 34;

		A[row][col] = elevation;

		count++;
	}

	// Start glut
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE);
	glutInitWindowSize(500,500);

	glutCreateWindow("Didier Arslanian - Assignment 2");

	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);

	glutDisplayFunc(display);
	glutReshapeFunc(reshape);
	glutKeyboardFunc(keyboard);

	glutMainLoop();
}